/* eslint one-var: 0, no-extend-native: 0 */
export default class OrgChart {
  constructor (options) {
    this._name = 'OrgChart'
    Promise.prototype.finally = function (callback) {
      let P = this.constructor
      return this.then(
        value => P.resolve(callback()).then(() => value),
        reason => P.resolve(callback()).then(() => {
          throw reason
        })
      )
    }

    let that = this,
      defaultOptions = {
        'nodeTitle': 'name',
        'nodeId': 'id',
        'toggleSiblingsResp': false,
        'depth': 999,
        'chartClass': '',
        'exportButton': false,
        'exportFilename': 'OrgChart',
        'parentNodeSymbol': 'fa-users',
        'draggable': false,
        'direction': 't2b',
        'pan': false,
        'zoom': false
      },
      opts = Object.assign(defaultOptions, options),
      data = this._cloneObj(opts.data),
      chart = document.createElement('div'),
      chartContainer = document.querySelector(opts.chartContainer)

    this.options = opts
    delete this.options.data
    this.chart = chart
    this.chartContainer = chartContainer
    this.chartContainer.innerHTML = ''
    chart.dataset.options = JSON.stringify(opts)
    chart.setAttribute('class', 'orgchart' + (opts.chartClass !== '' ? ' ' + opts.chartClass : '') +
      (opts.direction !== 't2b' ? ' ' + opts.direction : ''))
    if (typeof data === 'object') { // local json datasource
      this.buildHierarchy(chart, opts.ajaxURL ? data : this._attachRel(data, '00'), 0)
    } else if (typeof data === 'string' && data.startsWith('#')) { // ul datasource
      this.buildHierarchy(chart, this._buildJsonDS(document.querySelector(data).children[0]), 0)
    } else { // ajax datasource
      let spinner = document.createElement('i')

      spinner.setAttribute('class', 'fa fa-circle-o-notch fa-spin spinner')
      chart.appendChild(spinner)
      this._getJSON(data)
        .then(function (resp) {
          that.buildHierarchy(chart, opts.ajaxURL ? resp : that._attachRel(resp, '00'), 0)
        })
        .catch(function (err) {
          console.error('failed to fetch datasource for orgchart', err)
        })
        .finally(function () {
          let spinner = chart.querySelector('.spinner')
          spinner.parentNode.removeChild(spinner)
        })
    }

    //   绑定事件
    chartContainer.style.overflow = 'hidden'
    chart.addEventListener('mousedown', this._onPanStart.bind(this))
    chart.addEventListener('touchstart', this._onPanStart.bind(this))
    document.body.addEventListener('mouseup', this._onPanEnd.bind(this))
    document.body.addEventListener('touchend', this._onPanEnd.bind(this))

    // 鼠标滚轮缩放事件
    chartContainer.addEventListener('wheel', this._onWheeling.bind(this))

    // 当没有节点第一次拖动时
    if (chart.childElementCount === 0) {
      chart.classList.add('allowedDrop')
      this.isNull = true
      chart.ondragover = this._onDragOver
      chart.addEventListener('drop', this._creatRootNode.bind(this))
    }
    // append the export button to the chart-container
    chartContainer.appendChild(chart)
  }

  get name () {
    return this._name
  }
  // 工具函数
  _closest (el, fn) {
    return el && ((fn(el) && el !== this.chart) ? el : this._closest(el.parentNode, fn))
  }

  _siblings (el, expr) {
    return Array.from(el.parentNode.children).filter((child) => {
      if (child !== el) {
        if (expr) {
          return el.matches(expr)
        }
        return true
      }
      return false
    })
  }

  _removeClass (elements, classNames) {
    elements.forEach((el) => {
      if (classNames.indexOf(' ') > 0) {
        classNames.split(' ').forEach((className) => el.classList.remove(className))
      } else {
        el.classList.remove(classNames)
      }
    })
  }

  _getJSON (url) {
    return new Promise(function (resolve, reject) {
      let xhr = new XMLHttpRequest()

      function handler () {
        if (this.readyState !== 4) {
          return
        }
        if (this.status === 200) {
          resolve(JSON.parse(this.response))
        } else {
          reject(new Error(this.statusText))
        }
      }

      xhr.open('GET', url)
      xhr.onreadystatechange = handler
      xhr.responseType = 'json'
      // xhr.setRequestHeader('Accept', 'application/json');
      xhr.setRequestHeader('Content-Type', 'application/json')
      xhr.send()
    })
  }

  _attachRel (data, flags) {
    data.relationship = flags + (data.children && data.children.length > 0 ? 1 : 0)
    if (data.children) {
      for (let item of data.children) {
        this._attachRel(item, '1' + (data.children.length > 1 ? 1 : 0))
      }
    }
    return data
  }

  // define node click event handler
  _clickNode (event) {
    let clickedNode = event.currentTarget,
      focusedNode = this.chart.querySelector('.focused')

    if (focusedNode) {
      focusedNode.classList.remove('focused')
    }
    clickedNode.classList.add('focused')
  }

  _dispatchClickEvent (event) {
    this._clickNode(event)
  }

  // 拖动开始事件
  _onDragStart (event) {
    let nodeDiv = event.target,
      opts = this.options,
      isFirefox = /firefox/.test(window.navigator.userAgent.toLowerCase())

    if (isFirefox) {
      event.dataTransfer.setData('text/html', 'hack for firefox')
    }
    // if users enable zoom or direction options
    if (this.chart.style.transform) {
      let ghostNode, nodeCover

      if (!document.querySelector('.ghost-node')) {
        ghostNode = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
        ghostNode.classList.add('ghost-node')
        nodeCover = document.createElementNS('http://www.w3.org/2000/svg', 'rect')
        ghostNode.appendChild(nodeCover)
        this.chart.appendChild(ghostNode)
      } else {
        ghostNode = this.chart.querySelector(':scope > .ghost-node')
        nodeCover = ghostNode.children[0]
      }
      let transValues = this.chart.style.transform.split(','),
        scale = Math.abs(window.parseFloat((opts.direction === 't2b' || opts.direction === 'b2t') ? transValues[0].slice(transValues[0].indexOf('(') + 1) : transValues[1]))

      ghostNode.setAttribute('width', nodeDiv.offsetWidth)
      ghostNode.setAttribute('height', nodeDiv.offsetHeight)
      nodeCover.setAttribute('x', 5 * scale)
      nodeCover.setAttribute('y', 5 * scale)
      nodeCover.setAttribute('width', 120 * scale)
      nodeCover.setAttribute('height', 40 * scale)
      nodeCover.setAttribute('rx', 4 * scale)
      nodeCover.setAttribute('ry', 4 * scale)
      nodeCover.setAttribute('stroke-width', 1 * scale)
      let xOffset = event.offsetX * scale,
        yOffset = event.offsetY * scale

      if (opts.direction === 'l2r') {
        xOffset = event.offsetY * scale
        yOffset = event.offsetX * scale
      } else if (opts.direction === 'r2l') {
        xOffset = nodeDiv.offsetWidth - event.offsetY * scale
        yOffset = event.offsetX * scale
      } else if (opts.direction === 'b2t') {
        xOffset = nodeDiv.offsetWidth - event.offsetX * scale
        yOffset = nodeDiv.offsetHeight - event.offsetY * scale
      }
      if (isFirefox) { // hack for old version of Firefox(< 48.0)
        let ghostNodeWrapper = document.createElement('img')

        ghostNodeWrapper.src = 'data:image/svg+xml;utf8,' + (new XMLSerializer()).serializeToString(ghostNode)
        event.dataTransfer.setDragImage(ghostNodeWrapper, xOffset, yOffset)
        nodeCover.setAttribute('fill', 'rgb(255, 255, 255)')
        nodeCover.setAttribute('stroke', 'rgb(191, 0, 0)')
      } else {
        event.dataTransfer.setDragImage(ghostNode, xOffset, yOffset)
      }
    }
    let dragged = event.target,
      dragZone = this._closest(dragged, (el) => {
        return el.classList && el.classList.contains('nodes')
      }).parentNode.children[0].querySelector('.node'),
      dragHier = Array.from(this._closest(dragged, (el) => {
        return el.nodeName === 'TABLE'
      }).querySelectorAll('.node'))

    this.dragged = dragged
    Array.from(this.chart.querySelectorAll('.node')).forEach(function (node) {
      if (!dragHier.includes(node)) {
        if (opts.dropCriteria) {
          if (opts.dropCriteria(dragged, dragZone, node)) {
            node.classList.add('allowedDrop')
          }
        } else {
          node.classList.add('allowedDrop')
        }
      }
    })
  }

  // 拖动路过事件
  _onDragOver (event) {
    event.preventDefault()
    let dropZone = event.currentTarget
    if (!dropZone.classList.contains('allowedDrop')) {
      event.dataTransfer.dropEffect = 'none'
    }
  }

  // 拖动结束事件
  _onDragEnd (event) {
    Array.from(this.chart.querySelectorAll('.allowedDrop')).forEach(function (el) {
      el.classList.remove('allowedDrop')
    })
  }

  // 拖动放置事件
  _onDrop (event) {
    let dropZone = event.currentTarget,
      chart = this.chart,
      dragged = this.dragged,
      dragZone = this._closest(dragged, function (el) {
        return el.classList && el.classList.contains('nodes')
      })
    if (dragZone) {
      dragZone = dragZone.parentNode.children[0].children[0]
    }
    var dragId = '#org-node' + JSON.parse(dragged.getAttribute('data-source')).id
    this._removeClass(Array.from(chart.querySelectorAll('.allowedDrop')), 'allowedDrop')
    // firstly, deal with the hierarchy of drop zone
    if (!dropZone || !dropZone.parentNode.parentNode.nextElementSibling) { // if the drop zone is a leaf node
      dropZone.parentNode.setAttribute('colspan', 2)
      let table = this._closest(dropZone, function (el) {
          return el.nodeName === 'TABLE'
        }),
        upperTr = document.createElement('tr'),
        lowerTr = document.createElement('tr'),
        nodeTr = document.createElement('tr')
      upperTr.setAttribute('class', 'lines')
      upperTr.innerHTML = `<td colspan="2"><div class="downLine"></div></td>`
      lowerTr.setAttribute('class', 'lines')
      lowerTr.innerHTML = `<td class="rightLine">&nbsp;</td><td class="leftLine">&nbsp;</td>`
      nodeTr.setAttribute('class', 'nodes')
      let draggedTd = this._closest(dragged, (el) => el.nodeName === 'TABLE')
      if (draggedTd) {
        draggedTd = draggedTd.parentNode
      } else {
        draggedTd = this._createDraggedTd(dragged)
        this.chart.isAdd = true
      }
      if (this.chart.querySelector(dragId) && this.chart.isAdd) {
        this.chart.error = false
        return
      }
      table.appendChild(upperTr)
      table.appendChild(lowerTr)
      table.appendChild(nodeTr)
      nodeTr.appendChild(draggedTd)
    } else {
      let dropColspan = window.parseInt(dropZone.parentNode.colSpan) + 2
      dropZone.parentNode.setAttribute('colspan', dropColspan)
      dropZone.parentNode.parentNode.nextElementSibling.children[0].setAttribute('colspan', dropColspan)
      let temp = dropZone.parentNode.parentNode.nextElementSibling.nextElementSibling,
        leftline = document.createElement('td'),
        rightline = document.createElement('td')
      leftline.setAttribute('class', 'leftLine topLine')
      leftline.innerHTML = `&nbsp;`
      rightline.setAttribute('class', 'rightLine topLine')
      rightline.innerHTML = `&nbsp;`
      let draggedTd = this._closest(dragged, (el) => el.nodeName === 'TABLE')
      if (draggedTd) {
        draggedTd = draggedTd.parentNode
      } else {
        draggedTd = this._createDraggedTd(dragged)
        this.chart.isAdd = true
      }
      if (this.chart.isAdd && this.chart.querySelector(dragId)) {
        this.chart.error = false
        return
      }
      temp.insertBefore(leftline, temp.children[1])
      temp.insertBefore(rightline, temp.children[2])
      temp.nextElementSibling.appendChild(draggedTd)
    }
    // secondly, deal with the hierarchy of dragged node
    if (!dragZone) dragZone = document.createElement('td')
    let dragColSpan = window.parseInt(dragZone.colSpan)

    if (dragColSpan > 2) {
      dragZone.setAttribute('colspan', dragColSpan - 2)
      dragZone.parentNode.nextElementSibling.children[0].setAttribute('colspan', dragColSpan - 2)
      let temp = dragZone.parentNode.nextElementSibling.nextElementSibling
      temp.children[1].remove()
      temp.children[1].remove()
    } else {
      dragZone.removeAttribute('colspan')
      if (!this.chart.isAdd) Array.from(dragZone.parentNode.parentNode.children).slice(1).forEach((tr) => tr.remove())
    }
    let customE = new CustomEvent('nodedropped', {
      'detail': {
        'draggedNode': dragged,
        'dragZone': dragZone.children[0],
        'dropZone': dropZone
      }
    })
    chart.dispatchEvent(customE)
  }  // create node

  _creatRootNode (e) {
    this.chart.isAdd = true
    this.chart.appendChild(this._createTable(this.dragged))
  }

  _createNode (nodeData, level) {
    let that = this,
      opts = this.options

    return new Promise(function (resolve, reject) {
      if (nodeData.children) {
        for (let child of nodeData.children) {
          child.parentId = nodeData.id
        }
      }

      // construct the content of node
      let nodeDiv = document.createElement('div')

      delete nodeData.children
      nodeDiv.dataset.source = JSON.stringify(nodeData)
      if (nodeData[opts.nodeId]) {
        nodeDiv.id = 'org-node' + nodeData[opts.nodeId]
      }
      let inEdit = that.chart.dataset.inEdit,
        isHidden

      if (inEdit) {
        isHidden = inEdit === 'addChildren' ? ' slide-up' : ''
      } else {
        isHidden = level >= opts.depth ? ' slide-up' : ''
      }
      nodeDiv.setAttribute('class', 'node ' + (nodeData.className || '') + isHidden)
      if (opts.draggable) {
        nodeDiv.setAttribute('draggable', true)
      }
      if (nodeData.parentId) {
        nodeDiv.setAttribute('data-parent', nodeData.parentId)
      }
      nodeDiv.innerHTML = `
        <i class="iconfont icon-del org-item-del"></i>
        <div class="title">${nodeData[opts.nodeTitle]}</div>
        ${opts.nodeContent ? `<div class="org-item-content"></div>` : '<div class="org-item-content"><small class="text-primary item-edit">编辑</small></div>'}
      `

      nodeDiv.querySelector('.org-item-del').addEventListener('click', that._deleteTable.bind(that))
      nodeDiv.addEventListener('click', that._dispatchClickEvent.bind(that))
      if (opts.draggable) {
        nodeDiv.addEventListener('dragstart', that._onDragStart.bind(that))
        nodeDiv.addEventListener('dragover', that._onDragOver.bind(that))
        nodeDiv.addEventListener('dragend', that._onDragEnd.bind(that))
        nodeDiv.addEventListener('drop', that._onDrop.bind(that))
      }
      // allow user to append dom modification after finishing node create of orgchart
      if (opts.createNode) {
        opts.createNode(nodeDiv, nodeData)
      }
      resolve(nodeDiv)
    })
  }

  buildHierarchy (appendTo, nodeData, level, callback) {
    // Construct the node
    let that = this,
      opts = this.options,
      nodeWrapper,
      childNodes = nodeData.children,
      isVerticalNode = opts.verticalDepth && (level + 1) >= opts.verticalDepth
    if (childNodes && childNodes.length === 0) {
      childNodes = false
    }
    if (Object.keys(nodeData).length > 1) { // if nodeData has nested structure
      nodeWrapper = isVerticalNode ? appendTo : document.createElement('table')
      if (!isVerticalNode) {
        appendTo.appendChild(nodeWrapper)
      }
      this._createNode(nodeData, level)
        .then(function (nodeDiv) {
          if (isVerticalNode) {
            nodeWrapper.insertBefore(nodeDiv, nodeWrapper.firstChild)
          } else {
            let tr = document.createElement('tr')
            tr.setAttribute('class', 'value')
            tr.innerHTML = `
            <td ${childNodes ? `colspan="${childNodes.length * 2}"` : ''}>
            </td>
          `
            tr.children[0].appendChild(nodeDiv)
            nodeWrapper.insertBefore(tr, nodeWrapper.children[0] ? nodeWrapper.children[0] : null)
          }
          if (callback) {
            callback()
          }
        })
        .catch(function (err) {
          console.error('Failed to creat node', err)
        })
    }
    // Construct the inferior nodes and connectiong lines
    if (childNodes) {
      if (Object.keys(nodeData).length === 1) { // if nodeData is just an array
        nodeWrapper = appendTo
      }
      let isHidden,
        isVerticalLayer = opts.verticalDepth && (level + 2) >= opts.verticalDepth,
        inEdit = that.chart.dataset.inEdit

      if (inEdit) {
        isHidden = inEdit === 'addSiblings' ? '' : ' hidden'
      } else {
        isHidden = level + 1 >= opts.depth ? ' hidden' : ''
      }

      // draw the line close to parent node
      if (!isVerticalLayer) {
        let tr = document.createElement('tr')

        tr.setAttribute('class', 'lines' + isHidden)
        tr.innerHTML = `
          <td colspan="${childNodes.length * 2}">
            <div class="downLine"></div>
          </td>
        `
        nodeWrapper.appendChild(tr)
      }
      // draw the lines close to children nodes
      let lineLayer = document.createElement('tr')

      lineLayer.setAttribute('class', 'lines' + isHidden)
      lineLayer.innerHTML = `
        <td class="rightLine">&nbsp;</td>
        ${childNodes.slice(1).map(() => `
          <td class="leftLine topLine">&nbsp;</td>
          <td class="rightLine topLine">&nbsp;</td>
          `).join('')}
        <td class="leftLine">&nbsp;</td>
      `
      let nodeLayer

      if (isVerticalLayer) {
        nodeLayer = document.createElement('ul')
        if (isHidden) {
          nodeLayer.classList.add(isHidden.trim())
        }
        if (level + 2 === opts.verticalDepth) {
          let tr = document.createElement('tr')

          tr.setAttribute('class', 'verticalNodes' + isHidden)
          tr.innerHTML = `<td></td>`
          tr.firstChild.appendChild(nodeLayer)
          nodeWrapper.appendChild(tr)
        } else {
          nodeWrapper.appendChild(nodeLayer)
        }
      } else {
        nodeLayer = document.createElement('tr')
        nodeLayer.setAttribute('class', 'nodes' + isHidden)
        nodeWrapper.appendChild(lineLayer)
        nodeWrapper.appendChild(nodeLayer)
      }
      // recurse through children nodes
      childNodes.forEach((child) => {
        let nodeCell

        if (isVerticalLayer) {
          nodeCell = document.createElement('li')
        } else {
          nodeCell = document.createElement('td')
          nodeCell.setAttribute('colspan', 2)
        }
        nodeLayer.appendChild(nodeCell)
        that.buildHierarchy(nodeCell, child, level + 1, callback)
      })
    }
  }

  _clickChart (event) {
    let closestNode = this._closest(event.target, function (el) {
      return el.classList && el.classList.contains('node')
    })

    if (!closestNode && this.chart.querySelector('.node.focused')) {
      this.chart.querySelector('.node.focused').classList.remove('focused')
    }
  }

  _createDraggedTd (dragged) {
    var td = document.createElement('td')
    td.appendChild(this._createTable(dragged))
    return td
  }

  _createTable (dragged) {
    var table = document.createElement('table'),
      tr = document.createElement('tr'),
      intr = document.createElement('td')
    tr.classList.add('value')
    intr.appendChild(dragged)
    tr.appendChild(intr)
    table.appendChild(tr)
    return table
  }

  _deleteTable (e) {
    var parentTable = e.target.parentNode.parentNode.parentNode.parentNode
    var parent = parentTable.parentNode
    parent.removeChild(parentTable)
  }

  _cloneObj (obj) {
    var str = JSON.stringify(obj), // 系列化对象
      newobj = JSON.parse(str) // 还原
    return newobj
  }

  _onPanStart (event) {
    let chart = event.currentTarget

    if (this._closest(event.target, (el) => el.classList && el.classList.contains('node')) ||
      (event.touches && event.touches.length > 1)) {
      chart.dataset.panning = false
      return
    }
    chart.style.cursor = 'move'
    chart.dataset.panning = true

    let lastX = 0,
      lastY = 0,
      lastTf = window.getComputedStyle(chart).transform

    if (lastTf !== 'none') {
      let temp = lastTf.split(',')

      if (!lastTf.includes('3d')) {
        lastX = Number.parseInt(temp[4], 10)
        lastY = Number.parseInt(temp[5], 10)
      } else {
        lastX = Number.parseInt(temp[12], 10)
        lastY = Number.parseInt(temp[13], 10)
      }
    }
    let startX = 0,
      startY = 0

    if (!event.targetTouches) { // pan on desktop
      startX = event.pageX - lastX
      startY = event.pageY - lastY
    } else if (event.targetTouches.length === 1) { // pan on mobile device
      startX = event.targetTouches[0].pageX - lastX
      startY = event.targetTouches[0].pageY - lastY
    } else if (event.targetTouches.length > 1) {
      return
    }
    chart.dataset.panStart = JSON.stringify({'startX': startX, 'startY': startY})
    chart.addEventListener('mousemove', this._onPanning.bind(this))
    chart.addEventListener('touchmove', this._onPanning.bind(this))
  }

  _onPanning (event) {
    let chart = event.currentTarget

    if (chart.dataset.panning === 'false') {
      return
    }
    let newX = 0,
      newY = 0,
      panStart = JSON.parse(chart.dataset.panStart),
      startX = panStart.startX,
      startY = panStart.startY

    if (!event.targetTouches) { // pand on desktop
      newX = event.pageX - startX
      newY = event.pageY - startY
    } else if (event.targetTouches.length === 1) { // pan on mobile device
      newX = event.targetTouches[0].pageX - startX
      newY = event.targetTouches[0].pageY - startY
    } else if (event.targetTouches.length > 1) {
      return
    }
    let lastTf = window.getComputedStyle(chart).transform

    if (lastTf === 'none') {
      if (!lastTf.includes('3d')) {
        chart.style.transform = 'matrix(1, 0, 0, 1, ' + newX + ', ' + newY + ')'
      } else {
        chart.style.transform = 'matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, ' + newX + ', ' + newY + ', 0, 1)'
      }
    } else {
      let matrix = lastTf.split(',')

      if (!lastTf.includes('3d')) {
        matrix[4] = newX
        matrix[5] = newY + ')'
      } else {
        matrix[12] = newX
        matrix[13] = newY
      }
      chart.style.transform = matrix.join(',')
    }
  }

  _onPanEnd (event) {
    let chart = this.chart

    if (chart.dataset.panning === 'true') {
      chart.dataset.panning = false
      chart.style.cursor = 'default'
      document.body.removeEventListener('mousemove', this._onPanning)
      document.body.removeEventListener('touchmove', this._onPanning)
    }
  }

  _onWheeling (event) {
    event.preventDefault()

    let newScale = event.deltaY > 0 ? 0.8 : 1.2

    this._setChartScale(this.chart, newScale)
  }

  _setChartScale (chart, newScale) {
    let lastTf = window.getComputedStyle(chart).transform

    if (lastTf === 'none') {
      chart.style.transform = 'scale(' + newScale + ',' + newScale + ')'
    } else {
      let matrix = lastTf.split(',')
      if ((matrix[3] < 0.4 && newScale < 1) || (matrix[3] > 1.8 && newScale > 1)) return
      if (!lastTf.includes('3d')) {
        matrix[0] = 'matrix(' + newScale
        matrix[3] = newScale
        chart.style.transform = lastTf + ' scale(' + newScale + ',' + newScale + ')'
      } else {
        chart.style.transform = lastTf + ' scale3d(' + newScale + ',' + newScale + ', 1)'
      }
    }
    chart.dataset.scale = newScale
  }

}
